package de.metalcon.gwt.server.db;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.neo4j.graphdb.Direction;
import org.neo4j.graphdb.GraphDatabaseService;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.Relationship;
import org.neo4j.graphdb.RelationshipType;
import org.neo4j.graphdb.Transaction;
import org.neo4j.graphdb.index.Index;
import org.neo4j.graphdb.index.IndexHits;

import de.metalcon.gwt.server.db.nodes.Type;
import de.metalcon.gwt.server.exceptions.NodeAlreadyExistsException;
import de.metalcon.gwt.server.exceptions.RedundantCallException;
import de.metalcon.gwt.server.exceptions.UnknownPropertyException;

public abstract class ANode extends ANeoObject {
	private Node node;
	private static HashMap<String, ANode> instances = new HashMap<String, ANode>();

	protected ANode(GraphDatabaseService db, String globalKey) {
		super(db);

		if (instances.get(globalKey) != null) {
			throw new RedundantCallException("Instance of Node with the key " + globalKey + " already Exists!");
		}

		Transaction tx = db.beginTx();
		Index<Node> nodes = Indexes.anyNode.getIndex(db);
		if (node != null) {
			throw new NodeAlreadyExistsException("A node with the globalKey " + globalKey + " already Exists!");
		}

		try {
			node = db.createNode();
			addRelationshipTo(Type.GetTypeNode(db, getTypePrefix()), Relations.Type);

			nodes.add(node, "GlobalKey", globalKey);
			tx.success();
		} finally {
			tx.finish();
		}

		setGlobalKey(globalKey);
		instances.put(globalKey, this);
	}

	public ANode(Node node) {
		super(node.getGraphDatabase());
		this.node = node;

		if (instances.get(getGlobalKey()) != null) {
			throw new RedundantCallException("Instance of Node with the key " + getGlobalKey() + " already Exists!");
		}
		instances.put(getGlobalKey(), this);
	}

	public Node getNeoNode() {
		return node;
	}

	public String getGlobalKey() {
		return (String) getProperty("GlobalKey");
	}

	protected void setGlobalKey(String globalKey) {
		setProperty("GlobalKey", globalKey);
	}

	public Object getProperty(String key) {
		return node.getProperty(key);
	}

	public void setProperty(String key, Object value) {
		if (value.getClass() != getAllowedProperties().get(key)) {
			throw new UnknownPropertyException(key);
		}
		node.setProperty(key, value);
	}

	public HashMap<String, Object> getProperties() {
		HashMap<String, Object> map = new HashMap<String, Object>();
		for (String key : node.getPropertyKeys()) {
			map.put(key, (String) node.getProperty(key));
		}
		return map;
	}

	protected void deleteProperty(String key) {
		node.removeProperty(key);
	}

	protected void addRelationshipTo(ANode relatedNode, RelationshipType type) {
		node.createRelationshipTo(relatedNode.getNeoNode(), type);
	}

	public Type getTypeNode() {
		return Type.GetTypeNode(db, getTypePrefix());
	}

	/*
	 * Write content to DB
	 */
	@Override
	public void commit() {
		Transaction tx = db.beginTx();
		Index<Node> nodes = Indexes.anyNode.getIndex(db);
		try {
			if (node == null) {
				node = db.createNode();
				addRelationshipTo(Type.GetTypeNode(db, getTypePrefix()), Relations.Type);
			}

			for (Map.Entry<String, Object> entry : getProperties().entrySet()) {
				node.setProperty(entry.getKey(), entry.getValue());
			}

			nodes.add(node, "GlobalKey", getGlobalKey());

			tx.success();
		} finally {
			tx.finish();
		}
	}

	public List<ANode> getRelatedNodes(RelationshipType t, Direction dir) {
		LinkedList<ANode> nodes = new LinkedList<ANode>();

		for (Relationship rel : node.getRelationships(t, dir)) {
			if (dir == Direction.INCOMING || dir == Direction.BOTH) {
				nodes.add(ANode.Get(rel.getStartNode()));
			}

			if (dir == Direction.OUTGOING || dir == Direction.BOTH) {
				nodes.add(ANode.Get(rel.getEndNode()));
			}
		}

		return nodes;
	}

	/**
	 * 
	 * @param db
	 *            GraphDatabaseService to be used
	 * @param key
	 *            The key of the searched node
	 * @param typePrefix
	 *            The type prefix of the searched node
	 * @return An object of the class specified by <code>typePrefix</code> which
	 *         inherits from ANode or NULL
	 */
	public static ANode Get(GraphDatabaseService db, String key, char typePrefix) {
		String globalKey = GenerateGlobalKey(key, typePrefix);
		ANode mcNode = (ANode) instances.get(globalKey);
		if (mcNode == null) {
			Index<Node> nodes = Indexes.anyNode.getIndex(db);

			Node node = nodes.get("GlobalKey", globalKey).getSingle();
			if (node != null) {
				mcNode = generateFromNode(node);
			}
		}
		return mcNode;
	}

	public static ANode Get(Node node) {
		ANode mcNode = (ANode) instances.get(node.getProperty("GlobalKey"));
		if (mcNode == null) {
			return generateFromNode(node);
		}
		return mcNode;
	}

	public static ANode generateFromNode(Node node) {
		try {
			Class c;
			// Type nodes have only 2 chars as global key
			if (node.getProperty("GlobalKey").toString().length() == 2) {
				c = Type.class;
			} else {
				c = Type.GetTypeClassByPrefix(node.getProperty("GlobalKey").toString().charAt(0));
			}

			Class[] intArgsClass = new Class[] { Node.class };
			Constructor nodeConstructor;

			nodeConstructor = c.getConstructor(intArgsClass);
			return (ANode) nodeConstructor.newInstance(node);
		} catch (IllegalArgumentException e) {
			e.printStackTrace();
		} catch (InstantiationException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		} catch (InvocationTargetException e) {
			e.printStackTrace();
		} catch (SecurityException e) {
			e.printStackTrace();
		} catch (NoSuchMethodException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return null;
	}

	public abstract char getTypePrefix();

	protected abstract HashMap<String, Class> getAllowedProperties();

	public static String GenerateGlobalKey(String key, char typePrefix) {
		return typePrefix + ":" + key;
	}

	/**
	 * Generates an unused unique global key out of the proposed Strings in
	 * <code>proposals</code>
	 * 
	 * @param db
	 * @param proposals
	 * @param typePrefix
	 * @return
	 */
	public static String GenerateGlobalKey(GraphDatabaseService db, String[] proposals, char typePrefix) {
		Index<Node> allNodesIndex = Indexes.anyNode.getIndex(db);
		String key = "";
		for (int i = 0; i < proposals.length; i++) {
			if (i > 0) {
				key += "_";
			}
			key += proposals[i];
			IndexHits<Node> hits = allNodesIndex.get("GlobalKey", GenerateGlobalKey(key, typePrefix));
			if (hits.size() == 0) {
				return GenerateGlobalKey(key, typePrefix);
			}
		}

		int suffix = 1;
		while (true) {
			String newKey = key + suffix++;
			Node n = allNodesIndex.get("GlobalKey", GenerateGlobalKey(newKey, typePrefix)).getSingle();
			if (n == null) {
				return GenerateGlobalKey(newKey, typePrefix);
			}
		}
	}
}